All files / src/app/api/dev/tickets/[id]/time route.ts

0% Statements 0/143
100% Branches 0/0
0% Functions 0/1
0% Lines 0/143

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144                                                                                                                                                                                                                                                                                               
export const dynamic = "force-dynamic";

/**
 * Dev Ticket Time Tracking API
 * GET /api/dev/tickets/[id]/time - Get all time entries for a ticket
 * POST /api/dev/tickets/[id]/time - Log time to a ticket
 */

import { NextRequest, NextResponse } from 'next/server';
import { Session } from "next-auth";
import {
  withAdmin,
  withErrorHandling,
  successResponse,
  createdResponse,
  ApiError,
  ApiSuccessResponse,
  ApiErrorResponse } from "@/lib/api";
import { RouteContext } from "@/lib/api/middleware";
import type { AuthenticatedUser } from '@/lib/api/middleware/types';
import { prisma } from '@/lib/prisma';
import { CreateDevTimeEntrySchema } from '@/lib/validation/dev-ticket-schemas';
import { createTicketHistory, updateTicketActualHours } from '@/lib/dev-ticket';
import { logger } from '@/lib/logging';

interface RouteParams {
  params: Promise<{ id: string }>;
}

async function handleGet(
  request: NextRequest,
  context: RouteContext | undefined
): Promise<NextResponse<ApiSuccessResponse<unknown> | ApiErrorResponse>> {
  const { id } = await (context as RouteParams).params;

  // Verify ticket exists
  const ticket = await prisma.devTicket.findUnique({
    where: { id },
    select: { id: true, actualHours: true, estimatedHours: true } });

  if (!ticket) {
    throw ApiError.notFound('Ticket not found');
  }

  const timeEntries = await prisma.devTimeEntry.findMany({
    where: { ticketId: id },
    include: {
      user: {
        select: { id: true, name: true, email: true, image: true } } },
    orderBy: { date: 'desc' } });

  // Calculate totals
  const totalHours = timeEntries.reduce((sum, entry) => sum + entry.hours, 0);
  const byUser = timeEntries.reduce(
    (acc, entry) => {
      const userId = entry.userId;
      if (!acc[userId]) {
        acc[userId] = {
          user: entry.user,
          totalHours: 0,
          entries: 0 };
      }
      acc[userId].totalHours += entry.hours;
      acc[userId].entries += 1;
      return acc;
    },
    {} as Record<number, { user: typeof timeEntries[0]['user']; totalHours: number; entries: number }>
  );

  return successResponse({
    entries: timeEntries,
    summary: {
      totalHours,
      estimatedHours: ticket.estimatedHours,
      variance: ticket.estimatedHours ? totalHours - ticket.estimatedHours : null,
      byUser: Object.values(byUser) } });
}

async function handlePost(
  request: NextRequest,
  context: RouteContext | undefined,
  session: Session,
  user: AuthenticatedUser
): Promise<NextResponse<ApiSuccessResponse<unknown> | ApiErrorResponse>> {
  const { id } = await (context as RouteParams).params;
  const body = await request.json();

  const validationResult = CreateDevTimeEntrySchema.safeParse(body);
  if (!validationResult.success) {
    throw ApiError.validation(
      'Validation failed',
      validationResult.error.flatten().fieldErrors
    );
  }

  // Verify ticket exists
  const ticket = await prisma.devTicket.findUnique({
    where: { id },
    select: { id: true, ticketNumber: true } });

  if (!ticket) {
    throw ApiError.notFound('Ticket not found');
  }

  const data = validationResult.data;

  // Create time entry
  const timeEntry = await prisma.devTimeEntry.create({
    data: {
      ticketId: id,
      userId: user.id,
      hours: data.hours,
      description: data.description,
      date: data.date },
    include: {
      user: {
        select: { id: true, name: true, email: true, image: true } } } });

  // Update ticket's actual hours
  await updateTicketActualHours(id);

  // Record in history
  await createTicketHistory(
    id,
    user.id,
    'time_logged',
    'hours',
    null,
    data.hours.toString()
  );

  logger.info(`Logged ${data.hours}h to ticket ${ticket.ticketNumber}`, {
    category: 'DEV_TICKETS',
    ticketId: id,
    timeEntryId: timeEntry.id,
    hours: data.hours,
    userId: user.id });

  return createdResponse(timeEntry);
}

export const GET = withErrorHandling(withAdmin(handleGet));
export const POST = withErrorHandling(withAdmin(handlePost));